#include "OutStream.h"

#include <string.h>
#include <fan-llvm/core/Core.h>
#include "Buf.h"
#include "Charset.h"
#include "Endian.h"
#include "FanInt.h"
#include "IOErr.h"
#include "List.h"
#include "Map.h"
#include "Str.h"
#include "Type.h"
#include "UnsupportedErr.h"


#define kXmlEscNo          0x00
#define kXmlEscNewlines    0x01
#define kXmlEscQuotes      0x02
#define kXmlEscUnicode     0x04
#define kXmlEscControlChar 0x08
#define kXmlEscXmlChar     0x10

static uint8_t kXmlEscTable[128];
static Str* kNullStr;

static void OutStream_writePropStr(OutStream* self, Str* s);
static void OutStream_writeXmlEsc(OutStream* self, Int ch);

Type* OutStream_classType = NULL;
VTable* OutStream_classVTable = NULL;


/**********************************************
 *
 * Inline methods
 *
 **********************************************/


/* [inline] Void writeByte(UInt8 byte)*/
static inline void OutStream_writeByte(OutStream* self, uint8_t byte)
{
  OUT_STREAM_VTABLE(self)->write(self, byte);
}


/**********************************************
 *
 * Methods and fields
 *
 **********************************************/


/* Void make(OutStream? out) */
void OutStream_make(OutStream* self, OutStream* out)
{
  Obj_make(VAL_MAKE_REF(self));
  self->out = out;
  self->bigEndian = TRUE;
  self->charset = Charset_utf8();
}


/* Endian endian */
Endian* OutStream_endian(OutStream* self)
{
  return self->bigEndian ? Endian_big() : Endian_little();
}


/* Endian endian */
void OutStream_setEndian(OutStream* self, Endian* val)
{
  self->bigEndian = (val == Endian_big());
}


/* Charset charset */
Charset* OutStream_charset(OutStream* self)
{
  return self->charset;
}


/* Charset charset */
void OutStream_setCharset(OutStream* self, Charset* val)
{
  self->charset = val;
}


/* Int xmlEscNewlines */
Int OutStream_xmlEscNewlines(void)
{
  return kXmlEscNewlines;
}


/* Int xmlEscQuotes */
Int OutStream_xmlEscQuotes(void)
{
  return kXmlEscQuotes;
}


/* Int xmlEscUnicode */
Int OutStream_xmlEscUnicode(void)
{
  return kXmlEscUnicode;
}


/* This write(Int byte) */
OutStream* OutStream_write(OutStream* self, Int byte)
{
  if (self->out == NULL)
    exceptionThrow(UnsupportedErr_newWithFormattedMsg("%r wraps null OutStream", Type_qname(Obj_typeof(VAL_MAKE_REF(self)))));
  OUT_STREAM_VTABLE(self->out)->write(self->out, byte);
  return self;
}


/* This writeBuf(Buf buf, Int n) */
OutStream* OutStream_writeBuf_1(OutStream* self, Buf* buf)
{
  return OUT_STREAM_VTABLE(self)->writeBuf(self, buf, Buf_remaining(buf));
}


/* This writeBuf(Buf buf, Int n) */
OutStream* OutStream_writeBuf(OutStream* self, Buf* buf, Int n)
{
  if (self->out == NULL)
    exceptionThrow(UnsupportedErr_newWithFormattedMsg("%r wraps null OutStream", Type_qname(Obj_typeof(VAL_MAKE_REF(self)))));
  OUT_STREAM_VTABLE(self->out)->writeBuf(self->out, buf, n);
  return self;
}


/* This flush() */
OutStream* OutStream_flush(OutStream* self)
{
  if (self->out != NULL)
    OUT_STREAM_VTABLE(self->out)->flush(self->out);
  return self;
}


/* This sync() */
OutStream* OutStream_sync(OutStream* self)
{
  if (self->out != NULL)
    OUT_STREAM_VTABLE(self->out)->sync(self->out);
  return self;
}


/* Bool close() */
Bool OutStream_close(OutStream* self)
{
  if (self->out != NULL)
    return OUT_STREAM_VTABLE(self->out)->close(self->out);
  return TRUE;
}


/* This writeI2(Int n) */
OutStream* OutStream_writeI2(OutStream* self, Int n)
{
  int16_t v = n;
  if (self->bigEndian)
  {
    OutStream_writeByte(self, (v >> 8) & 0xFF);
    OutStream_writeByte(self, (v >> 0) & 0xFF);
  }
  else
  {
    OutStream_writeByte(self, (v >> 0) & 0xFF);
    OutStream_writeByte(self, (v >> 8) & 0xFF);
  }
  return self;
}


/* This writeI4(Int n) */
OutStream* OutStream_writeI4(OutStream* self, Int n)
{
  int32_t v = n;
  if (self->bigEndian)
  {
    OutStream_writeByte(self, (v >> 24) & 0xFF);
    OutStream_writeByte(self, (v >> 16) & 0xFF);
    OutStream_writeByte(self, (v >> 8) & 0xFF);
    OutStream_writeByte(self, (v >> 0) & 0xFF);
  }
  else
  {
    OutStream_writeByte(self, (v >> 0) & 0xFF);
    OutStream_writeByte(self, (v >> 8) & 0xFF);
    OutStream_writeByte(self, (v >> 16) & 0xFF);
    OutStream_writeByte(self, (v >> 24) & 0xFF);
  }
  return self;
}


/* This writeI8(Int n) */
OutStream* OutStream_writeI8(OutStream* self, Int n)
{
  int64_t v = n;
  if (self->bigEndian)
  {
    OutStream_writeByte(self, (v >> 56) & 0xFF);
    OutStream_writeByte(self, (v >> 48) & 0xFF);
    OutStream_writeByte(self, (v >> 40) & 0xFF);
    OutStream_writeByte(self, (v >> 32) & 0xFF);
    OutStream_writeByte(self, (v >> 24) & 0xFF);
    OutStream_writeByte(self, (v >> 16) & 0xFF);
    OutStream_writeByte(self, (v >> 8) & 0xFF);
    OutStream_writeByte(self, (v >> 0) & 0xFF);
  }
  else
  {
    OutStream_writeByte(self, (v >> 0) & 0xFF);
    OutStream_writeByte(self, (v >> 8) & 0xFF);
    OutStream_writeByte(self, (v >> 16) & 0xFF);
    OutStream_writeByte(self, (v >> 24) & 0xFF);
    OutStream_writeByte(self, (v >> 32) & 0xFF);
    OutStream_writeByte(self, (v >> 40) & 0xFF);
    OutStream_writeByte(self, (v >> 48) & 0xFF);
    OutStream_writeByte(self, (v >> 56) & 0xFF);
  }
  return self;
}


/* This writeF4(Float r) */
OutStream* OutStream_writeF4(OutStream* self, Float r)
{
  float v = r;
  int32_t* intPtr = (int32_t*)&v;
  return OutStream_writeI4(self, *intPtr);
}


/* This writeF8(Float r) */
OutStream* OutStream_writeF8(OutStream* self, Float r)
{
  double v = r;
  int64_t* intPtr = (int64_t*)&v;
  return OutStream_writeI8(self, *intPtr);
}


/* This writeDecimal(Decimal d) */
OutStream* OutStream_writeDecimal(OutStream* self, Decimal* d)
{
  return OutStream_writeUtf(self, OBJ_REF_VTABLE(d)->toStr(VAL_MAKE_REF(d)));
}


/* This writeBool(Bool b) */
OutStream* OutStream_writeBool(OutStream* self, Bool b)
{
  OutStream_writeByte(self, b ? 1 : 0);
  return self;
}


/* This writeUtf(Str s) */
OutStream* OutStream_writeUtf(OutStream* self, Str* s)
{
  Int slen = s->len;
  Int utflen = 0;

  // First we have to figure out the utf length
  for (Int i = 0; i < slen; i++)
  {
    Char c = s->buffer[i];
    if (c <= 0x007F)
      utflen += 1;
    else if (c > 0x07FF)
      utflen += 3;
    else
      utflen += 2;
  }

  // Sanity check
  if (utflen > 65536)
    exceptionThrow(IOErr_newWithFormattedMsg("String too big"));

  // Write length as 2 byte value
  OutStream_writeByte(self, (utflen >> 8) & 0xFF);
  OutStream_writeByte(self, (utflen >> 0) & 0xFF);

  // Write characters
  for (Int i = 0; i < slen; i++)
  {
    Char c = s->buffer[i];
    if (c <= 0x007F)
      OutStream_writeByte(self, c);
    else if (c > 0x07FF)
    {
      OutStream_writeByte(self, 0xE0 | ((c >> 12) & 0x0F));
      OutStream_writeByte(self, 0x80 | ((c >>  6) & 0x3F));
      OutStream_writeByte(self, 0x80 | ((c >>  0) & 0x3F));
    }
    else
    {
      OutStream_writeByte(self, 0xC0 | ((c >>  6) & 0x1F));
      OutStream_writeByte(self, 0x80 | ((c >>  0) & 0x3F));
    }
  }

  return self;
}


/* This writeChar(Int char) */
OutStream* OutStream_writeChar(OutStream* self, Int ch)
{
  if (OUT_STREAM_VTABLE(self)->writeChar != NULL)
    return OUT_STREAM_VTABLE(self)->writeChar(self, ch);

  if (self->out != NULL)
    OutStream_writeChar(self->out, ch);
  else
    Charset_encode(self->charset, ch, self);
  return self;
}


/* This writeChars(Str str, Int off, Int len) */
OutStream* OutStream_writeChars_1(OutStream* self, Str* str)
{
  return OutStream_writeChars(self, str, 0, Str_size(str));
}


/* This writeChars(Str str, Int off, Int len) */
OutStream* OutStream_writeChars_2(OutStream* self, Str* str, Int off)
{
  return OutStream_writeChars(self, str, off, Str_size(str) - off);
}


/* This writeChars(Str str, Int off, Int len) */
OutStream* OutStream_writeChars(OutStream* self, Str* str, Int off, Int len)
{
  if (OUT_STREAM_VTABLE(self)->writeChars != NULL)
    return OUT_STREAM_VTABLE(self)->writeChars(self, str, off, len);

  Int end = off + len;
  for (Int i = off; i < end; i++)
    OutStream_writeChar(self, Str_get(str, i));
  return self;
}


/* This print(Obj? s) */
OutStream* OutStream_print(OutStream* self, NullableObj s)
{
  Str* str = !VAL_IS_NULL(s) ? VAL_GET_REF(s) : kNullStr;
  return OutStream_writeChars_1(self, str);
}


/* This printLine(Obj? obj) */
OutStream* OutStream_printLine_0(OutStream* self)
{
  return OutStream_printLine(self, VAL_MAKE_REF(Str_defVal()));
}


/* This printLine(Obj? obj) */
OutStream* OutStream_printLine(OutStream* self, NullableObj obj)
{
  OutStream_print(self, obj);
  return OutStream_writeChar(self, '\n');
}


/* This writeObj(Obj? obj, [Str:Obj]? options) */
OutStream* OutStream_writeObj_1(OutStream* self, NullableObj obj)
{
  return OutStream_writeObj(self, obj, NULL);
}


/* This writeObj(Obj? obj, [Str:Obj]? options) */
OutStream* OutStream_writeObj(OutStream* self, NullableObj obj, Map* options)
{
  exceptionThrow(UnsupportedErr_newWithFormattedMsg("sys::OutStream.writeObj not implemented yet"));
  return NULL;
}


/* This writeProps([Str:Str] props, Bool close) */
OutStream* OutStream_writeProps_1(OutStream* self, Map* props)
{
  return OutStream_writeProps(self, props, TRUE);
}


/* This writeProps([Str:Str] props, Bool close) */
OutStream* OutStream_writeProps(OutStream* self, Map* props, Bool close)
{
  Charset* origCharset = self->charset;
  self->charset = Charset_utf8();

  ExceptionBlock block = exceptionBegin();
  if (block == ExceptionBlock_Try)
  {
    List* keys = Map_keys(props);
    Int size = List_size(keys);
    for (Int i = 0; i < size; i++)
    {
      Str* key = Obj_castToClass(List_get(keys, i), Str_classType, TRUE);
      Str* val = Obj_castToClass(Map_get_1(props, VAL_MAKE_REF(key)), Str_classType, TRUE);
      OutStream_writePropStr(self, key);
      OutStream_writeChar(self, '=');
      OutStream_writePropStr(self, val);
      OutStream_writeChar(self, '\n');
    }
  }
  else if (block == ExceptionBlock_Finally)
  {
    if (close)
      OUT_STREAM_VTABLE(self)->close(self);
    self->charset = origCharset;
  }
  exceptionEnd();

  return self;
}


/* [private] Void writePropStr(Str s) */
static void OutStream_writePropStr(OutStream* self, Str* s)
{
  Int len = s->len;
  for (Int i = 0; i < len; i++)
  {
    Char ch = s->buffer[i];
    Char peek = (i + 1 < len) ? s->buffer[i + 1] : 0;

    // Escape special chars
    switch (ch)
    {
      case '\n':
        OutStream_writeChar(self, '\\');
        OutStream_writeChar(self, 'n');
        continue;
      case '\r':
        OutStream_writeChar(self, '\\');
        OutStream_writeChar(self, 'r');
        continue;
      case '\t':
        OutStream_writeChar(self, '\\');
        OutStream_writeChar(self, 't');
        continue;
      case '\\':
        OutStream_writeChar(self, '\\');
        OutStream_writeChar(self, '\\');
        continue;
    }

    // Escape control chars, comments, and =
    if ((ch < ' ') || (ch == '/' && (peek == '/' || peek == '*')) || (ch == '='))
    {
      Int nib1 = VAL_GET_INT(Int_toDigit((ch >> 4) & 0x0F, 16));
      Int nib2 = VAL_GET_INT(Int_toDigit((ch >> 0) & 0x0F, 16));
      OutStream_writeChar(self, '\\');
      OutStream_writeChar(self, 'u');
      OutStream_writeChar(self, '0');
      OutStream_writeChar(self, '0');
      OutStream_writeChar(self, nib1);
      OutStream_writeChar(self, nib2);
      continue;
    }

    // Normal character
    OutStream_writeChar(self, ch);
  }
}


/* This writeXml(Str str, Int mode) */
OutStream* OutStream_writeXml_1(OutStream* self, Str* str)
{
  return OutStream_writeXml(self, str, 0);
}


/* This writeXml(Str str, Int mode) */
OutStream* OutStream_writeXml(OutStream* self, Str* str, Int mode)
{
  for (Int i = 0; i < str->len; i++)
  {
    Char ch = str->buffer[i];
    uint8_t escType = (ch <= 0x7F) ? kXmlEscTable[ch] : kXmlEscUnicode;
    switch (escType & (mode | kXmlEscControlChar | kXmlEscXmlChar))
    {
      case kXmlEscControlChar:
      case kXmlEscNewlines:
      case kXmlEscUnicode:
        OutStream_writeXmlEsc(self, ch);
        break;

      case kXmlEscQuotes:
        if (ch == '\'')
        {
          OutStream_writeChar(self, '&');
          OutStream_writeChar(self, 'a');
          OutStream_writeChar(self, 'p');
          OutStream_writeChar(self, 'o');
          OutStream_writeChar(self, 's');
          OutStream_writeChar(self, ';');
          break;
        }
        else if (ch == '"')
        {
          OutStream_writeChar(self, '&');
          OutStream_writeChar(self, 'q');
          OutStream_writeChar(self, 'u');
          OutStream_writeChar(self, 'o');
          OutStream_writeChar(self, 't');
          OutStream_writeChar(self, ';');
          break;
        }

      case kXmlEscXmlChar:
        if (ch == '<')
        {
          OutStream_writeChar(self, '&');
          OutStream_writeChar(self, 'l');
          OutStream_writeChar(self, 't');
          OutStream_writeChar(self, ';');
          break;
        }
        else if (ch == '>')
        {
          if (i == 0 || str->buffer[i - 1] == ']')
          {
            OutStream_writeChar(self, '&');
            OutStream_writeChar(self, 'g');
            OutStream_writeChar(self, 't');
            OutStream_writeChar(self, ';');
            break;
          }
        }
        else if (ch == '&')
        {
          OutStream_writeChar(self, '&');
          OutStream_writeChar(self, 'a');
          OutStream_writeChar(self, 'm');
          OutStream_writeChar(self, 'p');
          OutStream_writeChar(self, ';');
          break;
        }

      default:
        OutStream_writeChar(self, ch);
        break;
    }
  }

  return self;
}


/* [private] Void writeXmlEsc(Int ch) */
static void OutStream_writeXmlEsc(OutStream* self, Int ch)
{
  const char* hex = "0123456789abcdef";
  OutStream_writeChar(self, '&');
  OutStream_writeChar(self, '#');
  OutStream_writeChar(self, 'x');
  if (ch > 0xFF) {
    OutStream_writeChar(self, hex[(ch >> 12) & 0x0F]);
    OutStream_writeChar(self, hex[(ch >> 8) & 0x0F]);
  }
  OutStream_writeChar(self, hex[(ch >> 4) & 0x0F]);
  OutStream_writeChar(self, hex[(ch >> 0) & 0x0F]);
  OutStream_writeChar(self, ';');
}


/**********************************************
 *
 * Type-system functions
 *
 **********************************************/


Type* OutStream_initType(void)
{
  if (OutStream_classType == NULL)
  {
    Type* baseType = Obj_initType();
    VTable* vtable = OutStream_initVTable();
    Type* type = Type_new("OutStream", baseType, vtable, sizeof(OutStream), sizeof(OutStreamVTable));
    Type_registerCtorImplementation(type, "make", OutStream_make, 1);
    Type_registerMethodImplementation(type, "close", OutStream_close, 0);
    Type_registerMethodImplementation(type, "flush", OutStream_flush, 0);
    Type_registerMethodImplementation(type, "print", OutStream_print, 1);
    Type_registerMethodImplementation(type, "printLine", OutStream_printLine_0, 0);
    Type_registerMethodImplementation(type, "printLine", OutStream_printLine, 1);
    Type_registerMethodImplementation(type, "sync", OutStream_sync, 0);
    Type_registerMethodImplementation(type, "write", OutStream_write, 1);
    Type_registerMethodImplementation(type, "writeBool", OutStream_writeBool, 1);
    Type_registerMethodImplementation(type, "writeBuf", OutStream_writeBuf_1, 1);
    Type_registerMethodImplementation(type, "writeBuf", OutStream_writeBuf, 2);
    Type_registerMethodImplementation(type, "writeChar", OutStream_writeChar, 1);
    Type_registerMethodImplementation(type, "writeChars", OutStream_writeChars_1, 1);
    Type_registerMethodImplementation(type, "writeChars", OutStream_writeChars_2, 2);
    Type_registerMethodImplementation(type, "writeChars", OutStream_writeChars, 3);
    Type_registerMethodImplementation(type, "writeDecimal", OutStream_writeDecimal, 1);
    Type_registerMethodImplementation(type, "writeF4", OutStream_writeF4, 1);
    Type_registerMethodImplementation(type, "writeF8", OutStream_writeF8, 1);
    Type_registerMethodImplementation(type, "writeI2", OutStream_writeI2, 1);
    Type_registerMethodImplementation(type, "writeI4", OutStream_writeI4, 1);
    Type_registerMethodImplementation(type, "writeI8", OutStream_writeI8, 1);
    Type_registerMethodImplementation(type, "writeObj", OutStream_writeObj_1, 1);
    Type_registerMethodImplementation(type, "writeObj", OutStream_writeObj, 2);
    Type_registerMethodImplementation(type, "writeProps", OutStream_writeProps_1, 1);
    Type_registerMethodImplementation(type, "writeProps", OutStream_writeProps, 2);
    Type_registerMethodImplementation(type, "writeUtf", OutStream_writeUtf, 1);
    Type_registerMethodImplementation(type, "writeXml", OutStream_writeXml_1, 1);
    Type_registerMethodImplementation(type, "writeXml", OutStream_writeXml, 2);
    Type_registerFieldImplementation(type, "charset", OutStream_charset, OutStream_setCharset);
    Type_registerFieldImplementation(type, "endian", OutStream_endian, OutStream_setEndian);
    Type_registerFieldImplementation(type, "xmlEscNewlines", OutStream_xmlEscNewlines, NULL);
    Type_registerFieldImplementation(type, "xmlEscQuotes", OutStream_xmlEscQuotes, NULL);
    Type_registerFieldImplementation(type, "xmlEscUnicode", OutStream_xmlEscUnicode, NULL);
    OutStream_classType = type;
    OutStream_initStatic();
  }
  return OutStream_classType;
}


VTable* OutStream_initVTable(void)
{
  if (OutStream_classVTable == NULL)
  {
    VTable* vtable = memoryAlloc(sizeof(OutStreamVTable));
    memcpy(vtable, Obj_initVTable(), sizeof(FanObjVTable));
    ((OutStreamVTable*)vtable)->close = OutStream_close;
    ((OutStreamVTable*)vtable)->flush = OutStream_flush;
    ((OutStreamVTable*)vtable)->sync = OutStream_sync;
    ((OutStreamVTable*)vtable)->write = OutStream_write;
    ((OutStreamVTable*)vtable)->writeBuf_1 = OutStream_writeBuf_1;
    ((OutStreamVTable*)vtable)->writeBuf = OutStream_writeBuf;
    ((OutStreamVTable*)vtable)->writeChar = NULL;
    ((OutStreamVTable*)vtable)->writeChars = NULL;
    OutStream_classVTable = vtable;
  }
  return OutStream_classVTable;
}


void OutStream_initStatic(void)
{
  memset(kXmlEscTable, kXmlEscNo, sizeof(kXmlEscTable));
  for (Int ch = 0; ch < ' '; ch++)
    kXmlEscTable[ch] = kXmlEscControlChar;
  kXmlEscTable['\t'] = kXmlEscNo;
  kXmlEscTable['\n'] = kXmlEscNewlines;
  kXmlEscTable['\r'] = kXmlEscNewlines;
  kXmlEscTable['\''] = kXmlEscQuotes;
  kXmlEscTable['"'] = kXmlEscQuotes;
  kXmlEscTable['<'] = kXmlEscXmlChar;
  kXmlEscTable['>'] = kXmlEscXmlChar;
  kXmlEscTable['&'] = kXmlEscXmlChar;

  kNullStr = Str_newFromCStr("null");
}
